/*
 *  Copyright 2017 The WebRTC project authors. All Rights Reserved.
 *
 *  Use of this source code is governed by a BSD-style license
 *  that can be found in the LICENSE file in the root of the source
 *  tree. An additional intellectual property rights grant can be found
 *  in the file PATENTS.  All contributing project authors may
 *  be found in the AUTHORS file in the root of the source tree.
 */

package org.webrtc;

import android.annotation.TargetApi;
import android.media.MediaCodec;
import android.media.MediaCodecInfo;
import android.media.MediaFormat;
import android.opengl.GLES20;
import android.os.Bundle;

import androidx.annotation.Nullable;

import android.view.Surface;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Map;
import java.util.concurrent.BlockingDeque;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.TimeUnit;

import org.webrtc.ThreadUtils.ThreadChecker;

/**
 * Android hardware video encoder.
 *
 * @note This class is only supported on Android Kitkat and above.
 */
@TargetApi(19)
@SuppressWarnings("deprecation")
        // Cannot support API level 19 without using deprecated methods.
class HardwareVideoEncoder implements VideoEncoder {
    private static final String TAG = "HardwareVideoEncoder";

    // Bitrate modes - should be in sync with OMX_VIDEO_CONTROLRATETYPE defined
    // in OMX_Video.h
    private static final int VIDEO_ControlRateConstant = 2;
    // Key associated with the bitrate control mode value (above). Not present as a MediaFormat
    // constant until API level 21.
    private static final String KEY_BITRATE_MODE = "bitrate-mode";

    private static final int VIDEO_AVC_PROFILE_HIGH = 8;
    private static final int VIDEO_AVC_LEVEL_3 = 0x100;

    private static final int MAX_VIDEO_FRAMERATE = 30;

    // See MAX_ENCODER_Q_SIZE in androidmediaencoder.cc.
    private static final int MAX_ENCODER_Q_SIZE = 2;

    private static final int MEDIA_CODEC_RELEASE_TIMEOUT_MS = 5000;
    private static final int DEQUEUE_OUTPUT_BUFFER_TIMEOUT_US = 100000;

    /**
     * Keeps track of the number of output buffers that have been passed down the pipeline and not yet
     * released. We need to wait for this to go down to zero before operations invalidating the output
     * buffers, i.e., stop() and getOutputBuffers().
     */
    private static class BusyCount {
        private final Object countLock = new Object();
        private int count;

        public void increment() {
            synchronized (countLock) {
                count++;
            }
        }

        // This method may be called on an arbitrary thread.
        public void decrement() {
            synchronized (countLock) {
                count--;
                if (count == 0) {
                    countLock.notifyAll();
                }
            }
        }

        // The increment and waitForZero methods are called on the same thread (deliverEncodedImage,
        // running on the output thread). Hence, after waitForZero returns, the count will stay zero
        // until the same thread calls increment.
        public void waitForZero() {
            boolean wasInterrupted = false;
            synchronized (countLock) {
                while (count > 0) {
                    try {
                        countLock.wait();
                    } catch (InterruptedException e) {
                        Logging.e(TAG, "Interrupted while waiting on busy count", e);
                        wasInterrupted = true;
                    }
                }
            }

            if (wasInterrupted) {
                Thread.currentThread().interrupt();
            }
        }
    }

    // --- Initialized on construction.
    private final MediaCodecWrapperFactory mediaCodecWrapperFactory;
    private final String codecName;
    private final VideoCodecMimeType codecType;
    private final Integer surfaceColorFormat;
    private final Integer yuvColorFormat;
    private final YuvFormat yuvFormat;
    private final Map<String, String> params;
    private final int keyFrameIntervalSec; // Base interval for generating key frames.
    // Interval at which to force a key frame. Used to reduce color distortions caused by some
    // Qualcomm video encoders.
    private final long forcedKeyFrameNs;
    private final BitrateAdjuster bitrateAdjuster;
    // EGL context shared with the application.  Used to access texture inputs.
    private final EglBase14.Context sharedContext;

    // Drawer used to draw input textures onto the codec's input surface.
    private final GlRectDrawer textureDrawer = new GlRectDrawer();
    private final VideoFrameDrawer videoFrameDrawer = new VideoFrameDrawer();
    // A queue of EncodedImage.Builders that correspond to frames in the codec.  These builders are
    // pre-populated with all the information that can't be sent through MediaCodec.
    private final BlockingDeque<EncodedImage.Builder> outputBuilders = new LinkedBlockingDeque<>();

    private final ThreadChecker encodeThreadChecker = new ThreadChecker();
    private final ThreadChecker outputThreadChecker = new ThreadChecker();
    private final BusyCount outputBuffersBusyCount = new BusyCount();

    // --- Set on initialize and immutable until release.
    private Callback callback;
    private boolean automaticResizeOn;

    // --- Valid and immutable while an encoding session is running.
    @Nullable
    private MediaCodecWrapper codec;
    @Nullable
    private ByteBuffer[] outputBuffers;
    // Thread that delivers encoded frames to the user callback.
    @Nullable
    private Thread outputThread;

    // EGL base wrapping the shared texture context.  Holds hooks to both the shared context and the
    // input surface.  Making this base current allows textures from the context to be drawn onto the
    // surface.
    @Nullable
    private EglBase14 textureEglBase;
    // Input surface for the codec.  The encoder will draw input textures onto this surface.
    @Nullable
    private Surface textureInputSurface;

    private int width;
    private int height;
    private boolean useSurfaceMode;

    // --- Only accessed from the encoding thread.
    // Presentation timestamp of the last requested (or forced) key frame.
    private long lastKeyFrameNs;

    // --- Only accessed on the output thread.
    // Contents of the last observed config frame output by the MediaCodec. Used by H.264.
    @Nullable
    private ByteBuffer configBuffer;
    private int adjustedBitrate;

    // Whether the encoder is running.  Volatile so that the output thread can watch this value and
    // exit when the encoder stops.
    private volatile boolean running;
    // Any exception thrown during shutdown.  The output thread releases the MediaCodec and uses this
    // value to send exceptions thrown during release back to the encoder thread.
    @Nullable
    private volatile Exception shutdownException;

    /**
     * Creates a new HardwareVideoEncoder with the given codecName, codecType, colorFormat, key frame
     * intervals, and bitrateAdjuster.
     *
     * @param codecName               the hardware codec implementation to use
     * @param codecType               the type of the given video codec (eg. VP8, VP9, or H264)
     * @param surfaceColorFormat      color format for surface mode or null if not available
     * @param yuvColorFormat          color format for bytebuffer mode
     * @param keyFrameIntervalSec     interval in seconds between key frames; used to initialize the codec
     * @param forceKeyFrameIntervalMs interval at which to force a key frame if one is not requested;
     *                                used to reduce distortion caused by some codec implementations
     * @param bitrateAdjuster         algorithm used to correct codec implementations that do not produce the
     *                                desired bitrates
     * @throws IllegalArgumentException if colorFormat is unsupported
     */
    public HardwareVideoEncoder(MediaCodecWrapperFactory mediaCodecWrapperFactory, String codecName,
                                VideoCodecMimeType codecType, Integer surfaceColorFormat, Integer yuvColorFormat,
                                Map<String, String> params, int keyFrameIntervalSec, int forceKeyFrameIntervalMs,
                                BitrateAdjuster bitrateAdjuster, EglBase14.Context sharedContext) {
        this.mediaCodecWrapperFactory = mediaCodecWrapperFactory;
        this.codecName = codecName;
        this.codecType = codecType;
        this.surfaceColorFormat = surfaceColorFormat;
        this.yuvColorFormat = yuvColorFormat;
        this.yuvFormat = YuvFormat.valueOf(yuvColorFormat);
        this.params = params;
        this.keyFrameIntervalSec = keyFrameIntervalSec;
        this.forcedKeyFrameNs = TimeUnit.MILLISECONDS.toNanos(forceKeyFrameIntervalMs);
        this.bitrateAdjuster = bitrateAdjuster;
        this.sharedContext = sharedContext;

        // Allow construction on a different thread.
        encodeThreadChecker.detachThread();
    }

    @Override
    public VideoCodecStatus initEncode(Settings settings, Callback callback) {
        encodeThreadChecker.checkIsOnValidThread();

        this.callback = callback;
        automaticResizeOn = settings.automaticResizeOn;
        this.width = settings.width;
        this.height = settings.height;
        useSurfaceMode = canUseSurface();

        if (settings.startBitrate != 0 && settings.maxFramerate != 0) {
            bitrateAdjuster.setTargets(settings.startBitrate * 1000, settings.maxFramerate);
        }
        adjustedBitrate = bitrateAdjuster.getAdjustedBitrateBps();

        Logging.d(TAG,
                "initEncode: " + width + " x " + height + ". @ " + settings.startBitrate
                        + "kbps. Fps: " + settings.maxFramerate + " Use surface mode: " + useSurfaceMode);
        return initEncodeInternal();
    }

    private VideoCodecStatus initEncodeInternal() {
        encodeThreadChecker.checkIsOnValidThread();

        lastKeyFrameNs = -1;

        try {
            codec = mediaCodecWrapperFactory.createByCodecName(codecName);
        } catch (IOException | IllegalArgumentException e) {
            Logging.e(TAG, "Cannot create media encoder " + codecName);
            return VideoCodecStatus.FALLBACK_SOFTWARE;
        }

        final int colorFormat = useSurfaceMode ? surfaceColorFormat : yuvColorFormat;
        try {
            MediaFormat format = MediaFormat.createVideoFormat(codecType.mimeType(), width, height);
            format.setInteger(MediaFormat.KEY_BIT_RATE, adjustedBitrate);
            format.setInteger(KEY_BITRATE_MODE, VIDEO_ControlRateConstant);
            format.setInteger(MediaFormat.KEY_COLOR_FORMAT, colorFormat);
            format.setInteger(MediaFormat.KEY_FRAME_RATE, bitrateAdjuster.getCodecConfigFramerate());
            format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, keyFrameIntervalSec);
            if (codecType == VideoCodecMimeType.H264) {
                String profileLevelId = params.get(VideoCodecInfo.H264_FMTP_PROFILE_LEVEL_ID);
                if (profileLevelId == null) {
                    profileLevelId = VideoCodecInfo.H264_CONSTRAINED_BASELINE_3_1;
                }
                switch (profileLevelId) {
                    case VideoCodecInfo.H264_CONSTRAINED_HIGH_3_1:
                        format.setInteger("profile", VIDEO_AVC_PROFILE_HIGH);
                        format.setInteger("level", VIDEO_AVC_LEVEL_3);
                        break;
                    case VideoCodecInfo.H264_CONSTRAINED_BASELINE_3_1:
                        break;
                    default:
                        Logging.w(TAG, "Unknown profile level id: " + profileLevelId);
                }
            }
            Logging.d(TAG, "Format: " + format);
            codec.configure(
                    format, null /* surface */, null /* crypto */, MediaCodec.CONFIGURE_FLAG_ENCODE);

            if (useSurfaceMode) {
                textureEglBase = EglBase.createEgl14(sharedContext, EglBase.CONFIG_RECORDABLE);
                textureInputSurface = codec.createInputSurface();
                textureEglBase.createSurface(textureInputSurface);
                textureEglBase.makeCurrent();
            }

            codec.start();
            outputBuffers = codec.getOutputBuffers();
        } catch (IllegalStateException e) {
            Logging.e(TAG, "initEncodeInternal failed", e);
            release();
            return VideoCodecStatus.FALLBACK_SOFTWARE;
        }

        running = true;
        outputThreadChecker.detachThread();
        outputThread = createOutputThread();
        outputThread.start();

        return VideoCodecStatus.OK;
    }

    @Override
    public VideoCodecStatus release() {
        encodeThreadChecker.checkIsOnValidThread();

        final VideoCodecStatus returnValue;
        if (outputThread == null) {
            returnValue = VideoCodecStatus.OK;
        } else {
            // The outputThread actually stops and releases the codec once running is false.
            running = false;
            if (!ThreadUtils.joinUninterruptibly(outputThread, MEDIA_CODEC_RELEASE_TIMEOUT_MS)) {
                Logging.e(TAG, "Media encoder release timeout");
                returnValue = VideoCodecStatus.TIMEOUT;
            } else if (shutdownException != null) {
                // Log the exception and turn it into an error.
                Logging.e(TAG, "Media encoder release exception", shutdownException);
                returnValue = VideoCodecStatus.ERROR;
            } else {
                returnValue = VideoCodecStatus.OK;
            }
        }

        textureDrawer.release();
        videoFrameDrawer.release();
        if (textureEglBase != null) {
            textureEglBase.release();
            textureEglBase = null;
        }
        if (textureInputSurface != null) {
            textureInputSurface.release();
            textureInputSurface = null;
        }
        outputBuilders.clear();

        codec = null;
        outputBuffers = null;
        outputThread = null;

        // Allow changing thread after release.
        encodeThreadChecker.detachThread();

        return returnValue;
    }

    @Override
    public VideoCodecStatus encode(VideoFrame videoFrame, EncodeInfo encodeInfo) {
        encodeThreadChecker.checkIsOnValidThread();
        if (codec == null) {
            return VideoCodecStatus.UNINITIALIZED;
        }

        final VideoFrame.Buffer videoFrameBuffer = videoFrame.getBuffer();
        final boolean isTextureBuffer = videoFrameBuffer instanceof VideoFrame.TextureBuffer;

        // If input resolution changed, restart the codec with the new resolution.
        final int frameWidth = videoFrame.getBuffer().getWidth();
        final int frameHeight = videoFrame.getBuffer().getHeight();
        final boolean shouldUseSurfaceMode = canUseSurface() && isTextureBuffer;
        if (frameWidth != width || frameHeight != height || shouldUseSurfaceMode != useSurfaceMode) {
            VideoCodecStatus status = resetCodec(frameWidth, frameHeight, shouldUseSurfaceMode);
            if (status != VideoCodecStatus.OK) {
                return status;
            }
        }

        if (outputBuilders.size() > MAX_ENCODER_Q_SIZE) {
            // Too many frames in the encoder.  Drop this frame.
            Logging.e(TAG, "Dropped frame, encoder queue full");
            return VideoCodecStatus.NO_OUTPUT; // See webrtc bug 2887.
        }

        boolean requestedKeyFrame = false;
        for (EncodedImage.FrameType frameType : encodeInfo.frameTypes) {
            if (frameType == EncodedImage.FrameType.VideoFrameKey) {
                requestedKeyFrame = true;
            }
        }

        if (requestedKeyFrame || shouldForceKeyFrame(videoFrame.getTimestampNs())) {
            requestKeyFrame(videoFrame.getTimestampNs());
        }

        // Number of bytes in the video buffer. Y channel is sampled at one byte per pixel; U and V are
        // subsampled at one byte per four pixels.
        int bufferSize = videoFrameBuffer.getHeight() * videoFrameBuffer.getWidth() * 3 / 2;
        EncodedImage.Builder builder = EncodedImage.builder()
                .setCaptureTimeNs(videoFrame.getTimestampNs())
                .setEncodedWidth(videoFrame.getBuffer().getWidth())
                .setEncodedHeight(videoFrame.getBuffer().getHeight())
                .setRotation(videoFrame.getRotation());
        outputBuilders.offer(builder);

        final VideoCodecStatus returnValue;
        if (useSurfaceMode) {
            returnValue = encodeTextureBuffer(videoFrame);
        } else {
            returnValue = encodeByteBuffer(videoFrame, videoFrameBuffer, bufferSize);
        }

        // Check if the queue was successful.
        if (returnValue != VideoCodecStatus.OK) {
            // Keep the output builders in sync with buffers in the codec.
            outputBuilders.pollLast();
        }

        return returnValue;
    }

    private VideoCodecStatus encodeTextureBuffer(VideoFrame videoFrame) {
        encodeThreadChecker.checkIsOnValidThread();
        try {
            // TODO(perkj): glClear() shouldn't be necessary since every pixel is covered anyway,
            // but it's a workaround for bug webrtc:5147.
            GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
            // It is not necessary to release this frame because it doesn't own the buffer.
            VideoFrame derotatedFrame =
                    new VideoFrame(videoFrame.getBuffer(), 0 /* rotation */, videoFrame.getTimestampNs());
            videoFrameDrawer.drawFrame(derotatedFrame, textureDrawer, null /* additionalRenderMatrix */);
            textureEglBase.swapBuffers(videoFrame.getTimestampNs());
        } catch (RuntimeException e) {
            Logging.e(TAG, "encodeTexture failed", e);
            return VideoCodecStatus.ERROR;
        }
        return VideoCodecStatus.OK;
    }

    private VideoCodecStatus encodeByteBuffer(
            VideoFrame videoFrame, VideoFrame.Buffer videoFrameBuffer, int bufferSize) {
        encodeThreadChecker.checkIsOnValidThread();
        // Frame timestamp rounded to the nearest microsecond.
        long presentationTimestampUs = (videoFrame.getTimestampNs() + 500) / 1000;

        // No timeout.  Don't block for an input buffer, drop frames if the encoder falls behind.
        int index;
        try {
            index = codec.dequeueInputBuffer(0 /* timeout */);
        } catch (IllegalStateException e) {
            Logging.e(TAG, "dequeueInputBuffer failed", e);
            return VideoCodecStatus.ERROR;
        }

        if (index == -1) {
            // Encoder is falling behind.  No input buffers available.  Drop the frame.
            Logging.d(TAG, "Dropped frame, no input buffers available");
            return VideoCodecStatus.NO_OUTPUT; // See webrtc bug 2887.
        }

        ByteBuffer buffer;
        try {
            buffer = codec.getInputBuffers()[index];
        } catch (IllegalStateException e) {
            Logging.e(TAG, "getInputBuffers failed", e);
            return VideoCodecStatus.ERROR;
        }
        fillInputBuffer(buffer, videoFrameBuffer);

        try {
            codec.queueInputBuffer(
                    index, 0 /* offset */, bufferSize, presentationTimestampUs, 0 /* flags */);
        } catch (IllegalStateException e) {
            Logging.e(TAG, "queueInputBuffer failed", e);
            // IllegalStateException thrown when the codec is in the wrong state.
            return VideoCodecStatus.ERROR;
        }
        return VideoCodecStatus.OK;
    }

    @Override
    public VideoCodecStatus setRateAllocation(BitrateAllocation bitrateAllocation, int framerate) {
        encodeThreadChecker.checkIsOnValidThread();
        if (framerate > MAX_VIDEO_FRAMERATE) {
            framerate = MAX_VIDEO_FRAMERATE;
        }
        bitrateAdjuster.setTargets(bitrateAllocation.getSum(), framerate);
        return VideoCodecStatus.OK;
    }

    @Override
    public ScalingSettings getScalingSettings() {
        encodeThreadChecker.checkIsOnValidThread();
        if (automaticResizeOn) {
            if (codecType == VideoCodecMimeType.VP8) {
                final int kLowVp8QpThreshold = 29;
                final int kHighVp8QpThreshold = 95;
                return new ScalingSettings(kLowVp8QpThreshold, kHighVp8QpThreshold);
            } else if (codecType == VideoCodecMimeType.H264) {
                final int kLowH264QpThreshold = 24;
                final int kHighH264QpThreshold = 37;
                return new ScalingSettings(kLowH264QpThreshold, kHighH264QpThreshold);
            }
        }
        return ScalingSettings.OFF;
    }

    @Override
    public String getImplementationName() {
        return "HWEncoder";
    }

    private VideoCodecStatus resetCodec(int newWidth, int newHeight, boolean newUseSurfaceMode) {
        encodeThreadChecker.checkIsOnValidThread();
        VideoCodecStatus status = release();
        if (status != VideoCodecStatus.OK) {
            return status;
        }
        width = newWidth;
        height = newHeight;
        useSurfaceMode = newUseSurfaceMode;
        return initEncodeInternal();
    }

    private boolean shouldForceKeyFrame(long presentationTimestampNs) {
        encodeThreadChecker.checkIsOnValidThread();
        return forcedKeyFrameNs > 0 && presentationTimestampNs > lastKeyFrameNs + forcedKeyFrameNs;
    }

    private void requestKeyFrame(long presentationTimestampNs) {
        encodeThreadChecker.checkIsOnValidThread();
        // Ideally MediaCodec would honor BUFFER_FLAG_SYNC_FRAME so we could
        // indicate this in queueInputBuffer() below and guarantee _this_ frame
        // be encoded as a key frame, but sadly that flag is ignored.  Instead,
        // we request a key frame "soon".
        try {
            Bundle b = new Bundle();
            b.putInt(MediaCodec.PARAMETER_KEY_REQUEST_SYNC_FRAME, 0);
            codec.setParameters(b);
        } catch (IllegalStateException e) {
            Logging.e(TAG, "requestKeyFrame failed", e);
            return;
        }
        lastKeyFrameNs = presentationTimestampNs;
    }

    private Thread createOutputThread() {
        return new Thread() {
            @Override
            public void run() {
                while (running) {
                    deliverEncodedImage();
                }
                releaseCodecOnOutputThread();
            }
        };
    }

    // Visible for testing.
    protected void deliverEncodedImage() {
        outputThreadChecker.checkIsOnValidThread();
        try {
            MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
            int index = codec.dequeueOutputBuffer(info, DEQUEUE_OUTPUT_BUFFER_TIMEOUT_US);
            if (index < 0) {
                if (index == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
                    outputBuffersBusyCount.waitForZero();
                    outputBuffers = codec.getOutputBuffers();
                }
                return;
            }

            ByteBuffer codecOutputBuffer = outputBuffers[index];
            codecOutputBuffer.position(info.offset);
            codecOutputBuffer.limit(info.offset + info.size);

            if ((info.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
                Logging.d(TAG, "Config frame generated. Offset: " + info.offset + ". Size: " + info.size);
                configBuffer = ByteBuffer.allocateDirect(info.size);
                configBuffer.put(codecOutputBuffer);
            } else {
                bitrateAdjuster.reportEncodedFrame(info.size);
                if (adjustedBitrate != bitrateAdjuster.getAdjustedBitrateBps()) {
                    updateBitrate();
                }

                final boolean isKeyFrame = (info.flags & MediaCodec.BUFFER_FLAG_SYNC_FRAME) != 0;
                if (isKeyFrame) {
                    Logging.d(TAG, "Sync frame generated");
                }

                final ByteBuffer frameBuffer;
                if (isKeyFrame && codecType == VideoCodecMimeType.H264) {
                    Logging.d(TAG,
                            "Prepending config frame of size " + configBuffer.capacity()
                                    + " to output buffer with offset " + info.offset + ", size " + info.size);
                    // For H.264 key frame prepend SPS and PPS NALs at the start.
                    frameBuffer = ByteBuffer.allocateDirect(info.size + configBuffer.capacity());
                    configBuffer.rewind();
                    frameBuffer.put(configBuffer);
                    frameBuffer.put(codecOutputBuffer);
                    frameBuffer.rewind();
                } else {
                    frameBuffer = codecOutputBuffer.slice();
                }

                final EncodedImage.FrameType frameType = isKeyFrame
                        ? EncodedImage.FrameType.VideoFrameKey
                        : EncodedImage.FrameType.VideoFrameDelta;

                outputBuffersBusyCount.increment();
                EncodedImage.Builder builder = outputBuilders.poll();
                EncodedImage encodedImage = builder
                        .setBuffer(frameBuffer,
                                () -> {
                                    // This callback should not throw any exceptions since
                                    // it may be called on an arbitrary thread.
                                    // Check bug webrtc:11230 for more details.
                                    try {
                                        codec.releaseOutputBuffer(index, false);
                                    } catch (Exception e) {
                                        Logging.e(TAG, "releaseOutputBuffer failed", e);
                                    }
                                    outputBuffersBusyCount.decrement();
                                })
                        .setFrameType(frameType)
                        .createEncodedImage();
                // TODO(mellem):  Set codec-specific info.
                callback.onEncodedFrame(encodedImage, new CodecSpecificInfo());
                // Note that the callback may have retained the image.
                encodedImage.release();
            }
        } catch (IllegalStateException e) {
            Logging.e(TAG, "deliverOutput failed", e);
        }
    }

    private void releaseCodecOnOutputThread() {
        outputThreadChecker.checkIsOnValidThread();
        Logging.d(TAG, "Releasing MediaCodec on output thread");
        outputBuffersBusyCount.waitForZero();
        try {
            codec.stop();
        } catch (Exception e) {
            Logging.e(TAG, "Media encoder stop failed", e);
        }
        try {
            codec.release();
        } catch (Exception e) {
            Logging.e(TAG, "Media encoder release failed", e);
            // Propagate exceptions caught during release back to the main thread.
            shutdownException = e;
        }
        configBuffer = null;
        Logging.d(TAG, "Release on output thread done");
    }

    private VideoCodecStatus updateBitrate() {
        outputThreadChecker.checkIsOnValidThread();
        adjustedBitrate = bitrateAdjuster.getAdjustedBitrateBps();
        try {
            Bundle params = new Bundle();
            params.putInt(MediaCodec.PARAMETER_KEY_VIDEO_BITRATE, adjustedBitrate);
            codec.setParameters(params);
            return VideoCodecStatus.OK;
        } catch (IllegalStateException e) {
            Logging.e(TAG, "updateBitrate failed", e);
            return VideoCodecStatus.ERROR;
        }
    }

    private boolean canUseSurface() {
        return sharedContext != null && surfaceColorFormat != null;
    }

    // Visible for testing.
    protected void fillInputBuffer(ByteBuffer buffer, VideoFrame.Buffer videoFrameBuffer) {
        yuvFormat.fillBuffer(buffer, videoFrameBuffer);
    }

    /**
     * Enumeration of supported YUV color formats used for MediaCodec's input.
     */
    private enum YuvFormat {
        I420 {
            @Override
            void fillBuffer(ByteBuffer dstBuffer, VideoFrame.Buffer srcBuffer) {
                VideoFrame.I420Buffer i420 = srcBuffer.toI420();
                YuvHelper.I420Copy(i420.getDataY(), i420.getStrideY(), i420.getDataU(), i420.getStrideU(),
                        i420.getDataV(), i420.getStrideV(), dstBuffer, i420.getWidth(), i420.getHeight());
                i420.release();
            }
        },
        NV12 {
            @Override
            void fillBuffer(ByteBuffer dstBuffer, VideoFrame.Buffer srcBuffer) {
                VideoFrame.I420Buffer i420 = srcBuffer.toI420();
                YuvHelper.I420ToNV12(i420.getDataY(), i420.getStrideY(), i420.getDataU(), i420.getStrideU(),
                        i420.getDataV(), i420.getStrideV(), dstBuffer, i420.getWidth(), i420.getHeight());
                i420.release();
            }
        };

        abstract void fillBuffer(ByteBuffer dstBuffer, VideoFrame.Buffer srcBuffer);

        static YuvFormat valueOf(int colorFormat) {
            switch (colorFormat) {
                case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar:
                    return I420;
                case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar:
                case MediaCodecInfo.CodecCapabilities.COLOR_QCOM_FormatYUV420SemiPlanar:
                case MediaCodecUtils.COLOR_QCOM_FORMATYUV420PackedSemiPlanar32m:
                    return NV12;
                default:
                    throw new IllegalArgumentException("Unsupported colorFormat: " + colorFormat);
            }
        }
    }
}
